Skip to content

fix(mcp): drop the health verdict from clickstack_service_health - #3634

Merged
kodiakhq[bot] merged 3 commits into
mainfrom
brandon/brandon-service-health-poisoning
Nov 12, 2026
Merged

kodiakhq[bot] merged 3 commits into
mainfrom
brandon/brandon-service-health-poisoning

Conversation

@brandon-pereira

@brandon-pereira brandon-pereira commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Summary

clickstack_service_health no longer tells the agent whether a service is critical, degraded or healthy. A healthy service used to get "report this service as steady, no action needed". That verdict comes from service-wide totals, so a regression confined to one endpoint, pod or attribute segment can sit under every threshold while the tool tells the agent to stop looking. In the deploy-regression eval, main calls the service with the bug "Healthy".

Each service now carries only evidence:

Before After
Per-service field status: critical / degraded / healthy removed; flags and score stay
Summary, nothing crossed Healthy: … normal variance; report this service as steady, no action needed. No metric crossed a threshold vs yesterday: error rate 0.50% vs 0.49%, p95 120ms vs 115ms, requests 3000 vs 2900.
Summary, threshold crossed CRITICAL: error rate 8.00% vs 0.50%. Crossed thresholds: error rate 8.00% vs 0.50%.
Report summary statusCounts: {critical, degraded, healthy} servicesFlagged
Ordering status, then new errors, then score threshold-flagged services, then the rest; within each, new or growing errors first, then score
Span attributes mcp.service_health.critical_count, degraded_count mcp.service_health.flagged_count

The tool description and usage now say that thresholds apply to service-wide totals and point to clickstack_table breakdowns before a service is ruled out. "No flags is a valid result for a routine check" stays, so status reports are not pushed into escalating.

statusCounts and per-service status are gone from the response. Both are MCP output read by agents; nothing else in the repo consumes them.

Evals

A/B with packages/hdx-eval: branch (slot 99) vs main (slot 98), both built from 02f6fd060, identical seeded data, claude-opus-4-6.

Scenario Runs/arm Branch Main Δ Tool calls (branch / main)
service-health-check 3 76% 75% +1 7.7 / 5.7
error-root-cause 3 99% 98% +1 10.3 / 9.0
segmented-regression 8 75.5% 71.6% +3.9 14.9 / 17.1
deploy-regression 8 70.3% 74.6% −4.3 15.6 / 13.9
quiet-saturation 8 84.3% 88.8% −4.5 19.1 / 16.8
Average 81.0% 81.6% −0.6

The change is neutral on scores. Per-run spread within an arm is 15–20 points, so no difference is significant (the largest has t ≈ 1); segmented-regression went from −11 after 3 runs to +13 over the next 5. Routine checks did not get more alarmist: the judge scored service-health-check 8 points higher on the branch. Its regex false-alarm checks scored it lower, but they mostly match negated phrases such as "not ongoing" and "not escalation-worthy". Agents make a few more tool calls in most scenarios once the tool stops saying "no action needed".

How to reproduce

Starting point: local full stack (yarn dev) with trace data, and an MCP client such as npx @modelcontextprotocol/inspector connected to the local MCP endpoint (see MCP.md).

On main:

  1. Call clickstack_list_sources and copy a kind: "trace" source ID.
  2. Call clickstack_service_health with { "sourceId": "<id>" }.
  3. Each service has a status, and steady services read "Healthy: … no action needed."

On this branch:

  1. Repeat the call.
  2. There is no status field. Summaries state the crossed thresholds or the current vs comparison values, and summary.servicesFlagged replaces statusCounts.

How to test on Vercel preview

N/A — non-UI change.

References

The critical/degraded/healthy status and "no action needed" summaries
judged services from service-wide totals, which can miss a regression in
one endpoint, pod or segment and stop the agent investigating. Report the
threshold flags, score and raw values instead.

HDX-5877
@brandon-pereira
brandon-pereira requested a review from a team as a code owner November 12, 2026 18:09
@vercel

vercel Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hyperdx-oss Ready Ready Preview Nov 12, 2026 8:26pm UTC
hyperdx-storybook Ready Ready Preview Nov 12, 2026 8:26pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2a97fde

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/api Patch
@hyperdx/app Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the review/tier-2 Low risk — AI review + quick human skim label Nov 12, 2026
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

🔵 Tier 2 — Low Risk

Small, isolated change with no API route or data model modifications.

Why this tier:

  • Standard feature/fix — introduces new logic or modifies core functionality

Review process: AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns.
SLA: Resolve within 4 business hours.

Stats
  • Production files changed: 4
  • Production lines changed: 127 (+ 161 in test files, excluded from tier calculation)
  • Branch: brandon/brandon-service-health-poisoning
  • Author: brandon-pereira

To override this classification, remove the review/tier-2 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions

Copy link
Copy Markdown
Contributor

Duplicate code analysis

No new duplicate code introduced by this PR.


jscpd compared this branch against origin/main
across app, api and common-utils. Only new clones are listed; the codebase
currently holds 108 in total (1.18%).
Run yarn dupes locally to reproduce, or yarn dupes:all for the full picture.

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

PR Review

If you are a coding agent acting for the author, read this first.
Nothing below blocks merge automatically; a maintainer decides. What they will expect:

  • 🔴 critical or 🟠 major in code this PR adds or changes: fix before asking for review.
  • 🔵 minor in code this PR changes: your call. Fix if small, otherwise reply.
  • Anything about surrounding code, or asking you to widen the change (hoist a helper,
    dedupe with another file, fix other call sites): never fix here, whatever the
    severity. Reply with one sentence; if it is critical, say so plainly so a human sees it.

One commit per review round. After two rounds, stop and ask a maintainer to review
scope rather than addressing more automated findings. Full rule: AGENTS.md.

2 finding(s): 🔴 0 critical · 🟠 0 major · 🔵 2 minor

2 posted as inline comment(s) on the changed lines.


Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides.

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

This is a focused, well-tested refactor of the agent-facing clickstack_service_health MCP tool. The removed symbols (status, HealthStatus, STATUS_ORDER, statusCounts, critical_count/degraded_count) have no remaining consumers anywhere in the repo — the only surviving reference is a test asserting statusCounts is absent. Null handling in the new factual summary paths is safe, the servicesFlagged-vs-topN invariant holds (flagged services sort ahead of the slice), and the behavior change is covered by new unit and integration tests plus a changeset.

No P2 findings.

🔵 P3 nitpicks (3)
  • packages/api/src/mcp/tools/query/serviceHealth/summary.ts:101 — for a service absent from the comparison window (comparison is null, new_service flag set) the factual head renders error rate X% vs 0%, p95 Yms vs n/a, requests R vs 0, which can read to an agent as a jump from a real zero baseline rather than "no baseline exists" (mitigated by the new_service context line).

    • Fix: render the comparison side as "no baseline" / "n/a" when comparison is null instead of 0% and 0.
  • packages/api/src/mcp/tools/query/serviceHealth/report.ts:224 — the flagged-first ordering is now expressed twice, in compareAssessments (rank.ts:191) and inline in the shown sort here, so the two comparators can drift and silently break the servicesFlagged/topN consistency the comment relies on.

    • Fix: key the shown sort on the shared crossedThreshold comparator (or a single exported helper) so both orderings cannot diverge.
  • packages/api/src/mcp/tools/query/__tests__/serviceHealthRank.test.ts:333 — no test exercises the case where flagged services outnumber topN, i.e. servicesFlagged (counted over all ranked) exceeds servicesReturned while every shown service is still flagged.

    • Fix: add a buildReport case with more threshold-flagged services than topN asserting servicesFlagged > servicesReturned and that all returned services are flagged.

Reviewers: correctness, adversarial, api-contract, kieran-typescript, testing, maintainability, project-standards, previous-comments, agent-native, learnings-researcher were dispatched; findings synthesized from the orchestrator's full-diff analysis and the completed previous-comments reviewer (no actionable prior feedback — all 5 prior comments are automated bots).

Testing gaps: servicesFlagged > topN boundary is unverified; the absent-baseline (new_service) factual summary wording is asserted only indirectly.

No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

E2E Test Results

✅ All tests passed • 462 passed • 1 skipped • 1017s

Status Count
✅ Passed 462
❌ Failed 0
⚠️ Flaky 4
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

…alth

An unflagged service with new errors could push a threshold-flagged one
out of topN. Also print n/a instead of "n/ams" for a missing p95, pin the
no-comparison summary, and note the span attribute rename (review major,
minor, deep-review P2/P3).

describe('rankServices', () => {
it('sorts by status, then by score with volume as a multiplier', () => {
it('sorts flagged services first, then by score with volume as a multiplier', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Renamed 'flagged services first' test never exercises the new crossedThreshold key in compareAssessments

Every non-steady service in this fixture is flagged and has a higher score than steady, so the test still passes if the crossedThreshold term is deleted from compareAssessments (rank.ts:191). The new buildReport test only covers the re-sort in report.ts. Add a case where an unflagged service outscores a flagged one, for example a high-volume service going from 1.0% to 1.9% errors against a chronic high_error_rate service with score 0, and assert that the flagged service ranks first.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

'same window yesterday and ranked worst first. Each service gets a status ' +
'(critical / degraded / healthy), flags, and a one-line summary of which ' +
'changes crossed a threshold. New or growing error groups come first, ' +
'same window yesterday and ranked by how far each moved the wrong way. ' +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Tool description says services are 'ranked by how far each moved the wrong way', but the actual order is flagged first, then new_errors, then score

A chronic high_error_rate service with score 0, or a flagged service with new errors, ranks above an unflagged service with a larger score. The description is the first thing agents read, so state the real order to match USAGE (report.ts:144): 'threshold-flagged services first, then services with new or growing errors, then by magnitude'.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

@kodiakhq
kodiakhq Bot merged commit 7cfd333 into main Nov 12, 2026
28 checks passed
@kodiakhq
kodiakhq Bot deleted the brandon/brandon-service-health-poisoning branch November 12, 2026 20:31

This branch was successfully deployed

2 active deployments
Preview – hyperdx-oss — 2a97fde3 Deployed Nov 12, 2026 by vercel[bot]
Preview – hyperdx-storybook — 2a97fde3 Deployed Nov 12, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-2 Low risk — AI review + quick human skim

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants